// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.IO;
using System.Net;
using System.Web;
using System.Text;
using System.Collections.Generic;
using Microsoft.TeamFoundation.Migration.Toolkit;
using System.Diagnostics;
using Microsoft.Teamfoundation.Migration.VersionControl.SharePoint.Dws;
using System.Globalization;

namespace Microsoft.TeamFoundation.Migration.VersionControl.Wss
{
    public delegate T WebResponseCallback<T>(HttpWebResponse response);
	
    public class WssFileManager
	{
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")]
        public PutDocumentResult PutDocument(string uri, string fileName, string metaInfo, string comment)
        {
            Uri myUri = new Uri(uri);
            string webUrl, fileUrl;
            UrlToWebUrl(uri, out webUrl, out fileUrl);

            if (null == metaInfo)
            {
                metaInfo = string.Empty;
            }

            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException("File not found", fileName);
            }

            string postBody = String.Format(
                CultureInfo.InvariantCulture,
                "method=put+document&service_name=&document=[document_name={0};meta_info=[{1}]]&comment={2}&put_option=overwrite,migrationsemantics&keep_checked_out=false\n",
                HttpUtility.UrlEncode(fileUrl),
                HttpUtility.UrlEncode(metaInfo),
                HttpUtility.UrlEncode(comment));

            PutDocumentResult result;

            bool failedOnce = false;
            while (true)
            {
                result = null;

                // it's cheaper to assume the path exists and to detect failure than to
                // always check if the path exists (which is also an unlikely race condition).
                using (FileStream fs = File.OpenRead(fileName))
                {
                    SendRequest(
                        myUri.GetLeftPart(UriPartial.Authority) + webUrl + "/_vti_bin/_vti_aut/author.dll",
                        postBody, fs, documentUploadCallback, out result);
                }

                if (result.IsStatusPacket && !failedOnce && result.StatusResult.Status == 589830)
                {
                    failedOnce = true;

                    // the path to the item might be missing
                    EnsureParentToPathExists(uri);
                }
                else
                {
                    break;
                }
            }

            return result;
        }

        /// <summary>
        /// Make sure the parent path to the item exists. If not, create the path
        /// </summary>
        /// <param name="docUri">item to be checked for parent path</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")]
        public void EnsureParentToPathExists(string docUri)
        {
            string parent = getParentPath(docUri);

            if (!DoesPathExist(parent))
            {
                EnsureParentToPathExists(parent);
                CreatePath(parent);
            }
        }

        private static string getParentPath(string docUri)
        {
            int index = docUri.LastIndexOf('/');
            if (index != -1)
            {
                return docUri.Substring(0, index);
            }

            return null;
        }

        /// <summary>
        /// create the specified path in wss site
        /// </summary>
        /// <param name="docUri">uri of the path to be created</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")]
        public void CreatePath(string docUri)
        {
            if (DoesPathExist(docUri))
            {
                return;
            }
            Uri myUri = new Uri(docUri);
            string webUrl, fileUrl;
            UrlToWebUrl(docUri, out webUrl, out fileUrl);

            Dws dws = new Dws(myUri.GetLeftPart(UriPartial.Authority) + webUrl);
            dws.UseDefaultCredentials = true;
            string result = dws.CreateFolder(fileUrl);

            if (result != "<Result/>")
            {
                // Apparently Wss cannot handle directories that end in "_file" or "_files"
                // when these are detected it returns with the error string indicated.
                // These are "thicket" files (storage for components of multi-file documents) and cannot be migrated
                if (result == "<Error ID=\"2\">b</Error>" &&
                    (docUri.EndsWith("_file") || docUri.EndsWith("_files")))
                {
                    throw new VCInvalidPathException(
                        string.Format(CultureInfo.InvariantCulture, "The Wss path cannot be created: {0}", docUri));
                }

                throw new MigrationException(
                    string.Format(CultureInfo.InvariantCulture, "Unexpected result creating document workspace folder {0} - {1}",
                    fileUrl, result));
            }
        }

        /// <summary>
        /// Verify if a path already exists on the wss site
        /// </summary>
        /// <param name="docUri">uri to be verified</param>
        /// <returns>true if the path exists</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
        public static bool DoesPathExist(string docUri)
        {
            WebRequest wr = HttpWebRequest.Create(docUri);
            wr.UseDefaultCredentials = true;

            try
            {
                using (HttpWebResponse resp = (HttpWebResponse)wr.GetResponse())
                {
                    Debug.Assert(resp.StatusCode == HttpStatusCode.OK);
                    return true;
                }
            }
            catch (WebException we)
            {
                if (we.Response != null)
                {
                    if (((HttpWebResponse)we.Response).StatusCode == HttpStatusCode.NotFound)
                    {
                        return false;
                    }
                }

                throw;
            }
        }


        private PutDocumentResult documentUploadCallback(HttpWebResponse response)
        {
            return new PutDocumentResult(defaultCallback(response));
        }

        private VermeerRpcResult buildRpcResponsePacket(HttpWebResponse response)
        {
            return new VermeerRpcResult(defaultCallback(response));
        }


        private void UrlToWebUrl(string uri, out string webUrl, out string fileUrl)
        {
            Uri myUri = new Uri(uri);

            string postBody = String.Format(
                CultureInfo.InvariantCulture, 
                "method=url+to+web+url&url={0}&flags=0", 
                HttpUtility.UrlEncode(myUri.AbsolutePath));

            VermeerRpcResult result;

            SendRequest(
                myUri.GetLeftPart(UriPartial.Authority) + "/_vti_bin/shtml.dll/_vti_rpc",
                postBody, null, buildRpcResponsePacket, out result);

            if (result.IsStatusPacket)
            {
                throw new FPRpcException(result.StatusResult);
            }

            bool fileUrlWasSet = false;
            bool webUrlWasSet = false;

            fileUrl = null;
            webUrl = null;

            foreach (VermeerParagraph p in result.Paragraphs)
            {
                switch (p.Name)
                {
                    case "fileUrl":
                        Debug.Assert(!fileUrlWasSet);
                        fileUrl = HttpUtility.UrlDecode(p.Value);
                        fileUrlWasSet = true;
                        break;
                    case "webUrl":
                        Debug.Assert(!webUrlWasSet);
                        webUrl = HttpUtility.UrlDecode(p.Value);
                        webUrlWasSet = true;
                        break;
                }
            }

            if (!fileUrlWasSet || !webUrlWasSet)
            {
                throw new FPRpcException("The RPC method did not return the requested values.");
            }
        }

        private static void SendRequest<T>(string uri, string header, Stream content, WebResponseCallback<T> callback, out T result)
        {
            int attempt = 0;
            int maxAttempts = 3;

            while (attempt < maxAttempts)
            {
                try
                {
                    SendRequest_Internal(uri, header, content, callback, out result);
                    return;
                }
                catch (WebException we)
                {
                    // if the underlying connection was closed that usually means the parameters
                    // were invalid.  For example the uri might include an unescaped '&' in it.
                    if (we.Status == WebExceptionStatus.RequestCanceled)
                    {
                        TraceManager.TraceError(we, false);
                        throw;
                    }

                    attempt++;
                    if (attempt == maxAttempts)
                    {
                        TraceManager.TraceError(we, false);
                        throw;
                    }

                    TraceManager.TraceWarning("A retryable exception was encountered: " + we.Message);
                }
            }

            throw new MigrationException("Should never get here");
        }

        private static void SendRequest_Internal<T>(string uri, string header, Stream content, WebResponseCallback<T> callback, out T result)
        {
            if(string.IsNullOrEmpty(uri))
            {
                throw new ArgumentException("Url must not be null or empty", "uri");
            }

            WebRequest request = WebRequest.Create(uri);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.Headers.Add("X-Vermeer-Content-Type", "application/x-www-form-urlencoded");
            request.ConnectionGroupName = "WssFileManager";
            request.UseDefaultCredentials = true;

            using (Stream newStream = request.GetRequestStream())
            {
                if (header != null && header.Length > 0)
                {
                    writeToStream(newStream, header);
                }

                if (content != null && content.Length > 0)
                {
                    writeToStream(newStream, content);
                }
            }

            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                if (callback != null)
                {
                    result = callback(response);
                }
                else
                {
                    result = default(T);
                }
            }
        }

        private static string defaultCallback(HttpWebResponse response)
        {
            if (response.StatusCode != HttpStatusCode.OK)
            {
                throw new WebException(
                    WSS2TFSResources.WebOperationError,
                    null,
                    WebExceptionStatus.UnknownError,
                    response);
            }
            else
            {
                using (Stream receiveStream = response.GetResponseStream())
                using (StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8))
                {
                    return readStream.ReadToEnd();
                }
            }
        }

        private static void writeToStream(Stream stream, string content)
        {
            using (MemoryStream memStream = new MemoryStream(content.Length))
            {
                ASCIIEncoding encoding = new ASCIIEncoding();
                byte[] contentBytes = encoding.GetBytes(content);
                memStream.Write(contentBytes, 0, contentBytes.Length);
                memStream.Seek(0, SeekOrigin.Begin);

                writeToStream(stream, memStream);
            }
        }

        private static void writeToStream(Stream targetStream, Stream sourceStream)
        {
            byte[] b = new byte[4096];

            int read;
            while ((read = sourceStream.Read(b, 0, b.Length)) > 0)
            {
                targetStream.Write(b, 0, read);
            }
        }

        private VermeerRpcResult MoveDocument(string sourceUri, string targetUri)
        {
            TraceManager.TraceInformation("Moving Wss document {0} to {1}",
                sourceUri, targetUri);

            string sourceWebUrl, sourceFileUrl;
            UrlToWebUrl(sourceUri, out sourceWebUrl, out sourceFileUrl);

            string targetWebUrl, targetFileUrl;
            UrlToWebUrl(targetUri, out targetWebUrl, out targetFileUrl);

            string postBody = String.Format(
                CultureInfo.InvariantCulture,
                "method=move+document&service_name=&oldUrl={0}&newUrl={1}\n",
                HttpUtility.UrlEncode(sourceFileUrl),
                HttpUtility.UrlEncode(targetFileUrl));

            Uri tUri = new Uri(targetUri);

            VermeerRpcResult result;
            bool failedOnce = false;
            while (true)
            {
                result = null;

                // it's cheaper to assume the path exists and to detect failure than to
                // always check if the path exists (which is also an unlikely race condition).
                SendRequest(
                    tUri.GetLeftPart(UriPartial.Authority) + targetWebUrl + "/_vti_bin/_vti_aut/author.dll",
                    postBody, null, buildRpcResponsePacket, out result);

                if (result.IsStatusPacket && !failedOnce)
                {
                    failedOnce = true;

                    // the path to the item might be missing
                    EnsureParentToPathExists(targetUri);
                }
                else
                {
                    break;
                }
            }

            return result;
        }

        private VermeerRpcResult deleteDocument(string targetUri)
        {
            TraceManager.TraceInformation("Deleting Wss document {0}", targetUri);

            string targetWebUrl, targetFileUrl;
            UrlToWebUrl(targetUri, out targetWebUrl, out targetFileUrl);

            string postBody = String.Format(
                CultureInfo.InvariantCulture,
                "method=remove+documents&service_name=&url_list=[{0}]\n",
                HttpUtility.UrlEncode(targetFileUrl));

            Uri tUri = new Uri(targetUri);

            VermeerRpcResult result;
         
            SendRequest(
                tUri.GetLeftPart(UriPartial.Authority) + targetWebUrl + "/_vti_bin/_vti_aut/author.dll",
                postBody, null, buildRpcResponsePacket, out result);


            return result;
        }


        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")]
        public void RenameFile(string sourceUri, string targetUri)
        {
            VermeerRpcResult result = MoveDocument(sourceUri, targetUri);
            if (result.IsStatusPacket)
            {
                // in the case of thicket files (status 589924) should we try for an add/delete?
                throw new FPRpcException(
                    string.Format(CultureInfo.InvariantCulture, "Unable to rename Wss location {0} to {1}",
                    sourceUri, targetUri),
                    result.StatusResult);
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "0#")]
        public void DeleteDocument(string uri)
        {
            VermeerRpcResult result = deleteDocument(uri);
            if (result.IsStatusPacket)
            {
                // in the case of thicket files (status 589924) should we try for an add/delete?
                throw new FPRpcException(
                    string.Format(CultureInfo.InvariantCulture, "Unable to delete Wss document {0}",
                    uri),
                    result.StatusResult);
            }
        }
    }
}
